/*
 * Copyright (C) 2006-2007 Eskil Bylund
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

using System;
using System.Text;
using System.Text.RegularExpressions;

using Gtk;
using Mono.Unix;

namespace DCSharp.Gui
{
	public class ChatView : TextView
	{
		private TextMark endMark;
		private TextTag linkTag;

		private DateTime lastMessage;

		private bool hoveringOverLink;
		private static Gdk.Cursor handCursor;
		private static Gdk.Cursor normalCursor;

		private const string URL_REGEX = 
			@"((\b((news|http|https|ftp|file|irc)://|mailto:|(www|ftp)\.|\S*@\S*\.)|(^|\s)/\S+/|(^|\s)~/\S+)\S*\b/?)";

		private static Regex regex = new Regex(URL_REGEX,
			RegexOptions.IgnoreCase | RegexOptions.Compiled);

		private static Regex emoticonRegex;

		private static string[,] emoticons = {
			{"face-angel", "0:-)"},
			{"face-crying", ":'("},
			{"face-devil-grin", ">:-)"},
			{"face-devil-sad", ">:-("},
			{"face-glasses", "B-)"},
			{"face-kiss", ":-*"},
			{"face-monkey", ":-(|)"},
			{"face-plain", ":-|"},
			{"face-sad", ":-("},
			{"face-smile", ":-)"},
			{"face-smile-big", ":-D"},
			{"face-smirk", ":-!"},
			{"face-surprise", ":-0"},
			{"face-wink", ";-)"}
		};

		static ChatView()
		{
			// Create the emoticon regex
			StringBuilder regex = new StringBuilder(@"(^|(?<=[^\w]))(");
			for (int i = 0; i < emoticons.Length / 2; i++)
			{
				if (i != 0)
				{
					regex.Append("|");
				}
				string emoticon = Regex.Escape(emoticons[i, 1]);
				regex.Append(emoticon);
			}
			regex.Append(@")((?=[^\w])|$)");

			emoticonRegex = new Regex(regex.ToString(),
				RegexOptions.IgnoreCase | RegexOptions.Compiled);

			handCursor = new Gdk.Cursor(Gdk.CursorType.Hand2);
			normalCursor = new Gdk.Cursor(Gdk.CursorType.Xterm);
		}

		public ChatView() : base(new TextBuffer(null))
		{
			CursorVisible = false;
			Editable = false;
			WrapMode = WrapMode.WordChar;

			LeftMargin = 3;
			RightMargin = 3;

			lastMessage = DateTime.MinValue;

			// Block tags
			TextTag tag;
			tag = new TextTag("status");
			tag.Foreground = "PeachPuff4";
			Buffer.TagTable.Add(tag);

			tag = new TextTag("action");
			tag.Foreground = "brown4";
			tag.Style = Pango.Style.Italic;
			Buffer.TagTable.Add(tag);

			tag = new TextTag("error");
			tag.Foreground = "dark red";
			Buffer.TagTable.Add(tag);

			tag = new TextTag("time");
			tag.Foreground = "darkgrey";
			tag.Justification = Justification.Center;
			Buffer.TagTable.Add(tag);

			// Inline tags
			linkTag = new TextTag("link");
			linkTag.Foreground = "steelblue";
			linkTag.Underline = Pango.Underline.Single;
			Buffer.TagTable.Add(linkTag);

			tag = new TextTag("nick");
			tag.Foreground = "skyblue4";
			Buffer.TagTable.Add(tag);

			tag = new TextTag("self");
			tag.Foreground = "sea green";
			Buffer.TagTable.Add(tag);

			endMark = Buffer.CreateMark("end", Buffer.EndIter, true);
		}

		#region Methods

		public virtual void InsertError(string error)
		{
			InsertTextWithTags(" - " + error, "error");
		}

		public virtual void InsertStatus(string status)
		{
			InsertTextWithTags(" - " + status, "status");
		}

		public virtual void InsertMessage(string nick, string message, bool self)
		{
			if (message == null)
			{
				throw new ArgumentNullException("identity");
			}
			InsertTimestamp();

			// Check if the message is an action ("/me")
			if (message.StartsWith("/me "))
			{
				message = String.Format(" * {0} {1}", nick, message.Substring(4));
				InsertTextWithTags(message, "action");
			}
			else
			{
				string tags = "nick" + (self ? " self" : null);

				TextIter endIter = Buffer.EndIter;
				Buffer.InsertWithTagsByName(ref endIter, nick + ": ",
					tags.Split(' '));

				InsertText(message);
			}
		}

		public virtual void InsertTextWithTags(string text, params string[] tags)
		{
			if (tags == null)
			{
				throw new ArgumentNullException("tags");
			}
			InsertTimestamp();

			// Insert the text using InsertText to get links and emoticons
			// correctly inserted
			TextMark start = Buffer.CreateMark("startText", Buffer.EndIter, true);
			InsertText(text);

			// Apply the tags
			TextIter startIter = Buffer.GetIterAtMark(start);
			foreach (string tag in tags)
			{
				Buffer.ApplyTag(tag, startIter, Buffer.EndIter);
			}
			Buffer.DeleteMark(start);

			ScrollToEnd();
		}

		public virtual void InsertText(string text)
		{
			if (text == null)
			{
				throw new ArgumentNullException("text");
			}
			TextIter endIter = Buffer.EndIter;

			int pos = 0;
			foreach (Match match in regex.Matches(text))
			{
				Group group = match.Groups[1];

				if (pos < group.Index)
				{
					InsertTextWithEmoticons(ref endIter,
						text.Substring(pos, group.Index - pos));
				}

				Buffer.InsertWithTagsByName(ref endIter,
					text.Substring(group.Index, group.Length),
					"link");

				pos = group.Index + group.Length;
			}

			InsertTextWithEmoticons(ref endIter, text.Substring(pos) + "\n");
			ScrollToEnd();
		}

		protected override bool OnButtonPressEvent(Gdk.EventButton evnt)
		{
			if (evnt.Button != 1)
			{
				return base.OnButtonPressEvent(evnt);
			}

			int x, y;
			WindowToBufferCoords(TextWindowType.Text, (int)evnt.X, (int)evnt.Y,
				out x, out y);

			TextIter iter = GetIterAtLocation(x, y);
			if (iter.HasTag(linkTag))
			{
				TextIter end = iter;
				if (iter.BackwardToTagToggle(linkTag) &&
					end.ForwardToTagToggle(linkTag))
				{
					Util.OpenUrl(GetUrl(iter, end));
					return true;
				}
			}
			return base.OnButtonPressEvent(evnt);
		}

		protected override bool OnMotionNotifyEvent(Gdk.EventMotion evnt)
		{
			int pointerX, pointerY;
			Gdk.ModifierType pointerMask;

			Gdk.Window window = GetWindow(TextWindowType.Text);
			window.GetPointer(out pointerX, out pointerY, out pointerMask);

			int x, y;
			this.WindowToBufferCoords(TextWindowType.Widget, pointerX, pointerY,
				out x, out y);

			TextIter iter = GetIterAtLocation(x, y);
			bool hovering = iter.HasTag(linkTag);

			if (hovering != hoveringOverLink)
			{
				hoveringOverLink = hovering;
				if (hoveringOverLink)
				{
					window.Cursor = handCursor;
				}
				else
				{
					window.Cursor = normalCursor;
				}
			}
			return base.OnMotionNotifyEvent(evnt);
		}

		protected override void OnPopulatePopup(Menu menu)
		{
			MenuItem item = new SeparatorMenuItem();
			menu.Prepend(item);
			item.Show();

			// Clear menu item
			item = new ImageMenuItem(Stock.Clear, null);
			item.Sensitive = Buffer.CharCount > 0;
			item.Activated += delegate
			{
				Buffer.Clear();
				lastMessage = DateTime.MinValue;
			};
			menu.Prepend(item);
			item.Show();

			// Link context menu items
			int x, y, px, py;
			GetPointer(out px, out py);
			WindowToBufferCoords(TextWindowType.Text, px, py, out x, out y);

			TextIter iter = GetIterAtLocation(x, y);
			if (iter.HasTag(linkTag))
			{
				TextIter end = iter;
				if (iter.BackwardToTagToggle(linkTag) &&
					end.ForwardToTagToggle(linkTag))
				{
					item = new SeparatorMenuItem();
					menu.Prepend(item);
					item.Show();

					string url = GetUrl(iter, end);

					item = new MenuItem(Catalog.GetString("_Copy Link Address"));
					item.Activated += delegate
					{
						Clipboard clipboard = Clipboard.Get(
							Gdk.Atom.Intern("CLIPBOARD", true));
						clipboard.Text = url;

						clipboard = Clipboard.Get(
							Gdk.Atom.Intern("PRIMARY", true));
						clipboard.Text = url;
					};
					menu.Prepend(item);
					item.Show();

					item = new MenuItem(Catalog.GetString("_Open Link"));
					item.Activated += delegate
					{
						Util.OpenUrl(url);
					};
					menu.Prepend(item);
					item.Show();
				}
			}
		}

		private void InsertTextWithEmoticons(ref TextIter iter, string message)
		{
			int pos = 0;
			foreach (Match match in emoticonRegex.Matches(message))
			{
				if (pos < match.Index)
				{
					Buffer.Insert(ref iter,
						message.Substring(pos, match.Index - pos));
				}

				string emoticon = match.Value;
				string iconName = GetIconName(emoticon);

				Gdk.Pixbuf pixbuf = null;
				if (iconName != null)
				{
					pixbuf = IconManager.GetIcon(iconName, 16);
				}

				if (pixbuf != null)
				{
					Buffer.InsertPixbuf(ref iter, pixbuf);
				}
				else
				{
					Buffer.Insert(ref iter, emoticon);
				}
				pos = match.Index + match.Length;
			}
			Buffer.Insert(ref iter, message.Substring(pos));
		}

		private static string GetIconName(string emoticon)
		{
			for (int i = 0; i < emoticons.Length / 2; i++)
			{
				if (emoticons[i, 1] == emoticon)
				{
					return emoticons[i, 0];
				}
			}
			return null;
		}

		private bool InsertTimestamp()
		{
			DateTime now = DateTime.Now;
			TimeSpan elapsed = now - lastMessage;

			bool displayFullDateTime = now.DayOfYear != lastMessage.DayOfYear;
			lastMessage = now;

			TextIter endIter = Buffer.EndIter;
			if (displayFullDateTime)
			{
				Buffer.InsertWithTagsByName(ref endIter,
					String.Format("- {0} -\n", now.ToString("f")), "time");
				return true;
			}
			else if (elapsed.TotalMinutes > 5)
			{
				Buffer.InsertWithTagsByName(ref endIter,
					String.Format("- {0} -\n", now.ToString("t")), "time");
				return true;
			}
			return false;
		}

		// Based on code from Tomboy
		private static string GetUrl(TextIter start, TextIter end)
		{
			string url = start.GetText (end);

			// FIXME: Needed because the file match is greedy and
			// eats a leading space.
			url = url.Trim();

			// Simple url massaging.  Add to 'http://' to the front
			// of www.foo.com, 'ftp://' to ftp.foo.com,
			// 'mailto:' to alex@foo.com
			if (url.StartsWith("www."))
			{
				url = "http://" + url;
			}
			else if (url.StartsWith("ftp."))
			{
				//url = "ftp://" + url;
				url = "http://" + url;
			}
			else if (url.IndexOf("@") > 1 && url.IndexOf(".") > 3 &&
				!url.StartsWith("mailto:"))
			{
				url = "mailto:" + url;
			}
			return url;
		}

		private void ScrollToEnd()
		{
			if (Parent is ScrolledWindow)
			{
				ScrolledWindow scrolledWindow = Parent as ScrolledWindow;

				Adjustment adjustment = scrolledWindow.Vadjustment;
				bool scroll = adjustment.Value >= adjustment.Upper -
				adjustment.PageSize;

				if (scroll)
				{
					Buffer.MoveMark(endMark, Buffer.EndIter);
					ScrollMarkOnscreen(endMark);
				}
			}
		}

		#endregion
	}
}
